#include "precomp.h"
#include "../../Game/Game.h"

#define	WND_CLASSNAME					"Tmpl8Win"
#define	RETURN_ERROR(message, ...)		do{LOG_ERROR((message), __VA_ARGS__); return false;}while(0)
#define RETURN_FAILURE					return false
#define RETURN_SUCCESS					return true

static const DWORD dwStyleWindowed = WS_OVERLAPPEDWINDOW;
static const DWORD dwStyleFullscreen = WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;

namespace Tmpl {

static int TranslateKeycode(WPARAM keycode, LPARAM flags);

static struct WindowData
{
	bool active, minimized, fullscreen, resizeable, vsync;
	double systemTime;
	int w, h;

	DWORD dwStyle;
	HWND hWnd;
	HDC hDC;
	HGLRC hRC;
}
window;

static void OnResize(int w, int h)
{
	if(w == 0 && h == 0)
	{
		window.minimized = true;
	}
	else
	{
		window.minimized = false;
		window.w = w;
		window.h = h;

		glViewport(0, 0, w, h);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
 		glOrtho(0.0, (double)w, (double)h, 0.0, -128.0, 128.0);
		//glTranslated(0.375, 0.375, 0);
		Draw::ClipRectAdjust(w, h);
	}
}

// =================================================================================================
// Helper functions
// =================================================================================================

static DEVMODE GetDevMode(int width, int height)
{
	DEVMODE mode;
	memset(&mode, 0, sizeof(DEVMODE));
	mode.dmSize       = sizeof(DEVMODE);
	mode.dmPelsWidth  = width;
	mode.dmPelsHeight = height;
	mode.dmBitsPerPel = 32;
	mode.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
	return mode;
}

struct WinDims {int x, y, w, h;};
static WinDims GetWindowDimensions(DWORD dwStyle, int width, int height)
{
	WinDims dim;

	dim.x = GetSystemMetrics(SM_CXSCREEN)/2 - width/2;
	dim.y = GetSystemMetrics(SM_CYSCREEN)/2 - height/2;
	if(dim.x < 0) dim.x = 0;
	if(dim.y < 0) dim.y = 0;

	RECT rect = {0, 0, (long)width, (long)height};
	AdjustWindowRect(&rect, dwStyle, FALSE);
	dim.w = rect.right - rect.left;
	dim.h = rect.bottom - rect.top;

	return dim;
}

// =================================================================================================
// Window message callback
// =================================================================================================

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch(message)
	{
	case WM_CREATE:
		break;
	case WM_CLOSE:
	case WM_DESTROY:
		Config::exitApp = true;
		break;
	case WM_SIZE:
		OnResize((int)LOWORD(lParam), (int)HIWORD(lParam));
		break;
	case WM_LBUTTONDOWN:
		input.ClickMb(Mouse::LMB);
		break;
	case WM_LBUTTONUP:
		input.ReleaseMb(Mouse::LMB);
		break;
	case WM_MBUTTONDOWN:
		input.ClickMb(Mouse::MMB);
		break;
	case WM_MBUTTONUP:
		input.ReleaseMb(Mouse::MMB);
		break;
	case WM_RBUTTONDOWN:
		input.ClickMb(Mouse::RMB);
		break;
	case WM_RBUTTONUP:
		input.ReleaseMb(Mouse::RMB);
		break;
	case WM_MOUSEMOVE:
		input.SetMousePos(LOWORD(lParam), HIWORD(lParam));
		break;
	case WM_MOUSEWHEEL:
		input.ScrollMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam) > 0);
		break;
	case WM_SYSKEYDOWN:
	case WM_KEYDOWN:
		input.PressKey(TranslateKeycode(wParam, lParam));
		break;
	case WM_SYSKEYUP:
	case WM_KEYUP:
		input.ReleaseKey(TranslateKeycode(wParam, lParam));
		break;
	}
	return DefWindowProc(hWnd, message, wParam, lParam);
}

// =================================================================================================
// Window creation
// =================================================================================================

static bool TmplCreateWindow(const char* title, int width, int height, bool resizeable, bool fullscreen)
{
	HINSTANCE hInstance = GetModuleHandle(NULL);

	// Register window class
	WNDCLASSEX wc;
	memset(&wc, 0, sizeof(WNDCLASSEX));
	wc.cbSize			= sizeof(WNDCLASSEX);
	wc.style			= CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc		= (WNDPROC)WindowProc;
	wc.hInstance		= GetModuleHandle(NULL);
	wc.hIcon			= LoadIcon(NULL, IDI_WINLOGO);
	wc.hCursor			= LoadCursor(NULL, IDC_ARROW);
	wc.lpszClassName	= WND_CLASSNAME;
	if(!RegisterClassEx(&wc)) RETURN_ERROR("Failed to register WNDCLASS");

	// Adjust to fullscreen mode
	window.fullscreen = false;
	if(fullscreen && VideoMode(width, height, 32).IsValid())
	{
		DEVMODE mode = GetDevMode(width, height);
		if(ChangeDisplaySettings(&mode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			RETURN_ERROR("Failed to change display settings to fullscreen");
		}
		window.fullscreen = true;
	}
	
	// Determine window style
	window.dwStyle = dwStyleFullscreen;
	if((!window.fullscreen)) window.dwStyle = dwStyleWindowed;
	if((!window.fullscreen) && !resizeable) window.dwStyle &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
	window.resizeable = resizeable;

	// Determine window position and dimensions
	WinDims d = {0, 0, width, height};
	if(!fullscreen)	d = GetWindowDimensions(window.dwStyle, width, height);
	window.w = width, window.h = height;
	
	// Create window
	window.hWnd = CreateWindow(WND_CLASSNAME, title, window.dwStyle, d.x, d.y, d.w, d.h, 0, 0, hInstance, 0);
	if(!window.hWnd) RETURN_ERROR("Failed to create window (hWnd)");

	// Bring window into focus
	ShowWindow(window.hWnd, SW_SHOWNORMAL);
	SetForegroundWindow(window.hWnd);
	SetFocus(window.hWnd);

	RETURN_SUCCESS;
}

static void TmplDestroyWindow()
{
	if(IsWindow(window.hWnd)) DestroyWindow(window.hWnd);
	UnregisterClass(WND_CLASSNAME, GetModuleHandle(NULL));
}

// =================================================================================================
// OpenGl context management
// =================================================================================================

static bool TmplCreateGlContext()
{
	// Create the Device Context
	if(!(window.hDC = GetDC(window.hWnd))) RETURN_ERROR("Failed to create Device Context.");
	
	// Choose a pixel format
	unsigned int pixfmt;
	static PIXELFORMATDESCRIPTOR pfd = 
	{ 
		sizeof(PIXELFORMATDESCRIPTOR), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, 
		PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, PFD_MAIN_PLANE, 0, 0, 0, 0
	};

	if(!(pixfmt = ChoosePixelFormat(window.hDC, &pfd)))	RETURN_ERROR("Failed to find suitable Pixelformat.");
	if(!SetPixelFormat(window.hDC, pixfmt, &pfd))		RETURN_ERROR("Failed to set Pixelformat.");	
	if(!(window.hRC = wglCreateContext(window.hDC)))	RETURN_ERROR("Failed to create Rendering Context.");	
	if(!wglMakeCurrent(window.hDC, window.hRC))			RETURN_ERROR("Failed to activate Rendering Context.");

	RETURN_SUCCESS;
}

static void TmplDestroyGlContext()
{
	if(window.fullscreen) ChangeDisplaySettings(NULL, 0);
	wglMakeCurrent(NULL, NULL);
	if(window.hRC) wglDeleteContext(window.hRC);
	if(window.hDC && window.hWnd) ReleaseDC(window.hWnd, window.hDC);

	window.hRC = NULL;
	window.hDC = NULL;
}

static bool TmplInitOpenGl()
{
	glEnable(GL_TEXTURE_2D);
	glShadeModel(GL_SMOOTH);
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	glDisable(GL_CULL_FACE);
	glEnable(GL_SCISSOR_TEST);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);

	OnResize(WINDOW_W, WINDOW_H);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	return(glGetError() == 0);
}

// =================================================================================================
// System management
// =================================================================================================
int SystemGetCoresCount()
{
	SYSTEM_INFO sinfo;
	GetSystemInfo( &sinfo );
	return (int)sinfo.dwNumberOfProcessors;
}

// =================================================================================================
// Window management
// =================================================================================================

typedef bool (APIENTRY *PFNWGLSWAPINTERVALEXTPROC)(int interval);
extern PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;

WinProperties WinGetProperties()
{
	WinProperties props = 
	{
		window.active, window.minimized, window.fullscreen, window.resizeable,
		window.vsync, window.w, window.h
	};
	return props;
}

bool WinSetVsync(bool enable)
{
	PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;
	wglSwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalEXT");
    if(wglSwapIntervalEXT)
	{
		wglSwapIntervalEXT(enable ? 1 : 0);
		window.vsync = enable;
	}
	return (window.vsync == enable);
}

bool WinSetWindowSize(int width, int height)
{
	if(window.fullscreen)
	{
		if(!VideoMode(width, height, 32).IsValid()) RETURN_ERROR("Invalid video mode");

		// Apply fullscreen mode
		DEVMODE mode = GetDevMode(width, height);
		if(ChangeDisplaySettings(&mode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			RETURN_ERROR("Failed to change to window size in fullscreen mode");
		}

		// Resize the window so that it fits the entire screen
		SetWindowPos(window.hWnd, HWND_TOP, 0, 0, width, height, SWP_SHOWWINDOW);
	}
	else
	{
		WinDims d = GetWindowDimensions(window.dwStyle, width, height);
		SetWindowPos(window.hWnd, HWND_TOP, d.x, d.y, d.w, d.h, SWP_SHOWWINDOW | SWP_NOZORDER);
	}
	window.w = width;
	window.h = height;
	return true;
}

bool WinSetSizeAndFullscreen(bool fullscreen, int width, int height)
{
	if(fullscreen == window.fullscreen)
	{
		return WinSetWindowSize(width, height);
	}
	else if(fullscreen && !window.fullscreen)
	{
		if(!VideoMode(width, height, 32).IsValid()) RETURN_ERROR("Invalid video mode");

		// Apply fullscreen mode
		DEVMODE mode = GetDevMode(width, height);
		if(ChangeDisplaySettings(&mode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			RETURN_ERROR("Failed to change to fullscreen mode");
		}

		// Make the window flags compatible with fullscreen mode
		window.dwStyle = dwStyleFullscreen;
		SetWindowLong(window.hWnd, GWL_STYLE, window.dwStyle);
		SetWindowLong(window.hWnd, GWL_EXSTYLE, WS_EX_APPWINDOW);

		// Resize the window so that it fits the entire screen
		SetWindowPos(window.hWnd, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW);
	}
	else if(window.fullscreen && !fullscreen)
	{
		// Return to desktop
		ChangeDisplaySettings(NULL, 0);

		// Make the window flags compatible with fullscreen mode
		window.dwStyle = dwStyleWindowed;
		SetWindowLong(window.hWnd, GWL_STYLE, window.dwStyle);
		SetWindowLong(window.hWnd, GWL_EXSTYLE, WS_EX_APPWINDOW);

		// Restore window position and size
		WinDims d = GetWindowDimensions(window.dwStyle, width, height);
		SetWindowPos(window.hWnd, HWND_TOP, d.x, d.y, d.w, d.h, SWP_SHOWWINDOW | SWP_FRAMECHANGED);
		ShowWindow(window.hWnd, SW_SHOW);
	}
	window.w = width, window.h = height;
	window.fullscreen = fullscreen;
	RETURN_SUCCESS;
}

bool WinSetFullscreen(bool fullscreen)
{
	return WinSetSizeAndFullscreen(fullscreen, window.w, window.h);
}

// =================================================================================================
// Main loop
// =================================================================================================

int main(int argc, char* argv[])
{
	Log::Init();

	TmplCreateWindow(APP_NAME, WINDOW_W, WINDOW_H, RESIZEABLE, FULLSCREEN);
	TmplCreateGlContext();
	TmplInitOpenGl();

	input.BindTo(&mouse, &key);
	Font::InitDefault();
	Game::Init();

	window.systemTime = WinGetSystemTime();
	while(!Config::exitApp) 
	{
		input.Reset();

		// Window message loop
		MSG msg;
		while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		
		// Tick and display
		const double newSystemTime = WinGetSystemTime();
		Game::Tick((float)(newSystemTime - window.systemTime));
		window.systemTime = newSystemTime;
		SwapBuffers(window.hDC);
	
		// Close on Escape key
		if(key.IsPressed(TK_ESCAPE)) Config::exitApp = true;
		if(key.IsDown(TK_ALT) && key.IsPressed(TK_RETURN))
		{
			WinSetFullscreen(!window.fullscreen);
		}
	}
	TmplDestroyGlContext();
	TmplDestroyWindow();
	return 0;
}

// =================================================================================================
// Keycode translation
// =================================================================================================

static int TranslateKeycode(WPARAM keycode, LPARAM flags)
{
	if(keycode >= '0' && keycode <= '9') return keycode;
	if(keycode >= 'A' && keycode <= 'Z') return keycode - 'A' + TK_A;

	switch(keycode)
	{
	// Check the scancode to distinguish between left and right shift
	case VK_SHIFT:
	{
		static UINT lshift = MapVirtualKey(VK_LSHIFT, MAPVK_VK_TO_VSC);
		return (((flags & 0xFF0000) >> 16) == lshift ? TK_SHIFT_L : TK_SHIFT_R);
	}
	// Check the "extended" flag to distinguish between left and right alt
	case VK_MENU:		return ((HIWORD(flags) & KF_EXTENDED) ? TK_ALT_R	: TK_ALT_L);
	case VK_CONTROL:	return ((HIWORD(flags) & KF_EXTENDED) ? TK_CTRL_R	: TK_CTRL_L);

	// Other keys are reported properly
	case VK_LWIN:		return TK_SYSTEM_L;
	case VK_RWIN:		return TK_SYSTEM_R;
	case VK_APPS:		return TK_MENU;
	case VK_OEM_1:		return TK_SEMICOLON;
	case VK_OEM_2:		return TK_SLASH;
	case VK_OEM_PLUS:	return TK_EQUAL;
	case VK_OEM_MINUS:	return TK_DASH;
	case VK_OEM_4:		return TK_BRACKET_L;
	case VK_OEM_6:		return TK_BRACKET_R;
	case VK_OEM_COMMA:	return TK_COMMA;
	case VK_OEM_PERIOD:	return TK_PERIOD;
	case VK_OEM_7:		return TK_QUOTE;
	case VK_OEM_5:		return TK_BACKSLASH;
	case VK_OEM_3:		return TK_TILDE;
	case VK_ESCAPE:		return TK_ESCAPE;
	case VK_SPACE:		return TK_SPACE;
	case VK_RETURN:		return TK_RETURN;
	case VK_BACK:		return TK_BACKSPACE;
	case VK_TAB:		return TK_TAB;
	case VK_PRIOR:		return TK_PAGE_UP;
	case VK_NEXT:		return TK_PAGE_DOWN;
	case VK_END:		return TK_END;
	case VK_HOME:		return TK_HOME;
	case VK_INSERT:		return TK_INSERT;
	case VK_DELETE:		return TK_DELETE;
	case VK_ADD:		return TK_ADD;
	case VK_SUBTRACT:	return TK_SUBTRACT;
	case VK_MULTIPLY:	return TK_MULTIPLY;
	case VK_DIVIDE:		return TK_DIVIDE;
	case VK_PAUSE:		return TK_PAUSE;
	case VK_LEFT:		return TK_LEFT;
	case VK_RIGHT:		return TK_RIGHT;
	case VK_UP:			return TK_UP;
	case VK_DOWN:		return TK_DOWN;

	case VK_F1:			return TK_F1;
	case VK_F2:			return TK_F2;
	case VK_F3:			return TK_F3;
	case VK_F4:			return TK_F4;
	case VK_F5:			return TK_F5;
	case VK_F6:			return TK_F6;
	case VK_F7:			return TK_F7;
	case VK_F8:			return TK_F8;
	case VK_F9:			return TK_F9;
	case VK_F10:		return TK_F10;
	case VK_F11:		return TK_F11;
	case VK_F12:		return TK_F12;
	case VK_F13:		return TK_F13;
	case VK_F14:		return TK_F14;
	case VK_F15:		return TK_F15;

	case VK_NUMPAD0:	return TK_NUMPAD0;
	case VK_NUMPAD1:	return TK_NUMPAD1;
	case VK_NUMPAD2:	return TK_NUMPAD2;
	case VK_NUMPAD3:	return TK_NUMPAD3;
	case VK_NUMPAD4:	return TK_NUMPAD4;
	case VK_NUMPAD5:	return TK_NUMPAD5;
	case VK_NUMPAD6:	return TK_NUMPAD6;
	case VK_NUMPAD7:	return TK_NUMPAD7;
	case VK_NUMPAD8:	return TK_NUMPAD8;
	case VK_NUMPAD9:	return TK_NUMPAD9;
	}
	return 0;
}

// ===================================================================================
// VideoMode implementation
// ===================================================================================

int VideoMode::myModeCount = 0;
VideoMode* VideoMode::myModes = 0;

static bool CompareVideoMode(const VideoMode& v1, const VideoMode& v2)
{
	if(v1.bitsPerPixel != v2.bitsPerPixel) return (v1.bitsPerPixel > v2.bitsPerPixel);
	if(v1.width != v2.width) return (v1.width > v2.width);
	return (v1.height > v2.height);
}

static void SortVideoModes(VideoMode* modes, int size)
{
	for(int i=size-1; i>=0; i--) for(int j=1; j<=i; j++)
	{
		if(CompareVideoMode(modes[j], modes[j-1]))
		{
			VideoMode tmp = modes[j-1];
			modes[j-1] = modes[j];
			modes[j] = tmp;
		}
	}
}

static VideoMode* FindVideoMode(VideoMode* modes, int size, const VideoMode& mode)
{
	for(int i=0; i<size; ++i)
	{
		if(modes[i].width == mode.width && modes[i].height == mode.height &&
		   modes[i].bitsPerPixel == mode.bitsPerPixel) return &modes[i];
	}
	return 0;
}

bool VideoMode::IsValid() const
{
	if(!myModes) myReadModes();
	return (FindVideoMode(myModes, myModeCount, *this) != 0);
}

const VideoMode* VideoMode::GetModes()
{
	if(!myModes) myReadModes();
	return myModes;
}

int VideoMode::GetModeCount()
{
	if(!myModes) myReadModes();
	return myModeCount;
}

VideoMode VideoMode::GetDesktopMode()
{
	DEVMODE devmode;
	devmode.dmSize = sizeof(DEVMODE);
	EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devmode);
	return VideoMode(devmode.dmPelsWidth, devmode.dmPelsHeight, devmode.dmBitsPerPel);
}

void VideoMode::myReadModes()
{
	if(myModes) free(myModes);
	int allocSize = 2;
	myModes = (VideoMode*)malloc(allocSize * sizeof(VideoMode));

    // Enumerate all available video modes for primary display adapter
    DEVMODE devmode;
    devmode.dmSize = sizeof(DEVMODE);
    for(int i=0; EnumDisplaySettings(NULL, i, &devmode); ++i)
    {
        VideoMode mode(devmode.dmPelsWidth, devmode.dmPelsHeight, devmode.dmBitsPerPel);
		if(!FindVideoMode(myModes, myModeCount, mode) && mode.bitsPerPixel == 32)
		{
			myModes[myModeCount++] = mode;
			if(myModeCount == allocSize)
			{
				const int newSize = (allocSize<<=1) * sizeof(VideoMode);
				myModes = (VideoMode*)realloc(myModes, newSize);
			}
		}
	}
 	SortVideoModes(myModes, myModeCount);
}

}; // namespace Tmpl

// ===================================================================================
// Windows main
// ===================================================================================

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	return Tmpl::main(__argc, __argv);
}